/*
 * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.inject.cdi.se;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import javax.ws.rs.Path;
import javax.ws.rs.core.Application;
import javax.ws.rs.ext.Provider;

import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.enterprise.inject.spi.ProcessInjectionTarget;
import javax.enterprise.inject.spi.WithAnnotations;

import org.glassfish.jersey.inject.cdi.se.bean.BeanHelper;
import org.glassfish.jersey.inject.cdi.se.injector.JerseyInjectionTarget;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.internal.inject.Binding;
import org.glassfish.jersey.internal.inject.ClassBinding;
import org.glassfish.jersey.internal.inject.InjectionResolver;
import org.glassfish.jersey.internal.inject.InjectionResolverBinding;
import org.glassfish.jersey.internal.inject.InstanceBinding;
import org.glassfish.jersey.internal.inject.SupplierClassBinding;
import org.glassfish.jersey.internal.inject.SupplierInstanceBinding;

import org.jboss.weld.injection.producer.BasicInjectionTarget;

/**
 * CDI extension that handles CDI bootstrap events and registers Jersey's internally used components and components registered
 * using {@link Application}.
 *
 * @author Petr Bouda
 */
class SeBeanRegisterExtension implements Extension {

    private final AbstractBinder bindings;

    private final List<JerseyInjectionTarget> jerseyInjectionTargets = new ArrayList<>();

    /**
     * Creates a new extension that registers Jersey's internally used components and components registered using
     * {@link Application}.
     * <p>
     * TODO: Probably will be changed during the migration from CDI SE to JAVA EE environment.
     *
     * @param bindings all register beans using Jersey.
     */
    SeBeanRegisterExtension(AbstractBinder bindings) {
        this.bindings = bindings;
    }

    /**
     * Ignores the classes which are manually added using bindings (through {@link Application} class) and scanned by CDI.
     * The manual adding is privileged and the beans scanned using CDI are ignored.
     * <p>
     * TODO: The method counts with the condition that the all bindings are known before the CDI scanning has been started,
     * can be changed during the migration from CDI SE to JAVA EE environment.
     *
     * @param pat processed type.
     * @param <T> type of the scanned bean.
     */
    public <T> void ignoreManuallyRegisteredComponents(
            @Observes @WithAnnotations({ Path.class, Provider.class }) ProcessAnnotatedType<T> pat) {
        for (Binding binding : bindings.getBindings()) {
            if (ClassBinding.class.isAssignableFrom(binding.getClass())) {
                ClassBinding<?> classBinding = (ClassBinding<?>) binding;
                if (pat.getAnnotatedType().getJavaClass() == classBinding.getService()) {
                    pat.veto();
                    return;
                }
            } else if (InstanceBinding.class.isAssignableFrom(binding.getClass())) {
                InstanceBinding<?> instanceBinding = (InstanceBinding<?>) binding;
                if (pat.getAnnotatedType().getJavaClass() == instanceBinding.getService().getClass()) {
                    pat.veto();
                    return;
                }
            }
        }
    }

    /**
     * Wraps all JAR-RS components by Jersey-specific injection target.
     *
     * @param pit process injection target.
     * @param <T> type of the processed injection target.
     */
    public <T> void observeInjectionTarget(@Observes ProcessInjectionTarget<T> pit) {
        BasicInjectionTarget<T> it = (BasicInjectionTarget<T>) pit.getInjectionTarget();
        JerseyInjectionTarget<T> jerseyInjectionTarget =
                new JerseyInjectionTarget<>(it, pit.getAnnotatedType().getJavaClass());
        jerseyInjectionTargets.add(jerseyInjectionTarget);
        pit.setInjectionTarget(jerseyInjectionTarget);
    }

    /**
     * Takes all registered bindings and registers them in {@link BeanManager}.
     * <p>
     * Method should register only Jersey internal components and class/instances registered using {@link Application}. Registered
     * classes/instances have priority therefore CDI scanning should veto these classes/instances during {
     *
     * @param abd         {@code AfterBeanDiscovery} event.
     * @param beanManager current {@code BeanManager}.
     * @link ProcessAnnotatedType} bootstrap phase.
     */
    public void registerBeans(@Observes AfterBeanDiscovery abd, BeanManager beanManager) {
        Collection<Binding> bindings = this.bindings.getBindings();

        /*
         * Filters out only registered InjectionResolvers.
         */
        List<InjectionResolver> injectionResolvers = bindings.stream()
                .filter(binding -> InjectionResolverBinding.class.isAssignableFrom(binding.getClass()))
                .map(InjectionResolverBinding.class::cast)
                .map(InjectionResolverBinding::getResolver)
                .collect(Collectors.toList());

        /*
         * Provide registered InjectionResolvers to Jersey's components which has been discovered by CDI in
         * ProcessInjectionTarget bootstrap phase.
         */
        jerseyInjectionTargets.forEach(injectionTarget -> injectionTarget.setInjectionResolvers(injectionResolvers));

        for (Binding binding : bindings) {
            if (ClassBinding.class.isAssignableFrom(binding.getClass())) {
                BeanHelper.registerBean((ClassBinding<?>) binding, abd, injectionResolvers, beanManager);

            } else if (InstanceBinding.class.isAssignableFrom(binding.getClass())) {
                BeanHelper.registerBean((InstanceBinding<?>) binding, abd, injectionResolvers);

            } else if (SupplierClassBinding.class.isAssignableFrom(binding.getClass())) {
                BeanHelper.registerSupplier((SupplierClassBinding<?>) binding, abd, injectionResolvers, beanManager);

            } else if (SupplierInstanceBinding.class.isAssignableFrom(binding.getClass())) {
                BeanHelper.registerSupplier((SupplierInstanceBinding<?>) binding, abd, beanManager);
            }
        }

        abd.addBean(new RequestScopeBean(beanManager));
    }
}
